// Cheat module
// modified to read cheat file from memory, by regret
#include "burner.h"
#include "cheat.h"
#include "mem_file.h" // memory file

static wchar_t szCheatFilename[MAX_PATH];
vector<Mem_File*> cheatTempFiles; // save temp files

static Mem_File* getIncludeFile(const wchar_t* name);

static void cheatError(const wchar_t* filename, int nLineNumber, const CheatInfo* pCheat, const wchar_t* pszInfo, const wchar_t* pszLine)
{
	FBAPopupAddText(PUF_TEXT_NO_TRANSLATE, L"Cheat file %s is malformed.\nPlease remove or repair the file.\n\n", filename);
	if (pCheat) {
		FBAPopupAddText(PUF_TEXT_NO_TRANSLATE, L"Parse error at line %i, in cheat \"%s\".\n", nLineNumber, pCheat->szCheatName);
	} else {
		FBAPopupAddText(PUF_TEXT_NO_TRANSLATE, L"Parse error at line %i.\n", nLineNumber);
	}

	if (pszInfo) {
		FBAPopupAddText(PUF_TEXT_NO_TRANSLATE, L"Problem:\t%s.\n", pszInfo);
	}
	if (pszLine) {
		FBAPopupAddText(PUF_TEXT_NO_TRANSLATE, L"Text:\t%s\n", pszLine);
	}

	FBAPopupDisplay(PUF_TYPE_ERROR);
}

// ==> load cheat from memory, added by regret
static int parseMemCheatFile(Mem_File* file)
{
#define INSIDE_NOTHING (0xFFFF & (1 << (sizeof(wchar_t) * 8) - 1))

	if (!file) {
		return 1;
	}

	bool utf8File = false;
	// read file header
	unsigned char header[4] = { 0, };
	file->read((char*)header, sizearray(header), NULL);

	// check if it's a UTF-8 file
	if (header[0] == 0xEF && header[1] == 0xBB && header[2] == 0xBF) {
		utf8File = true;
	}
	file->seek(0);

	if (utf8File) {
		file->seek(3);
	}
	file->set_utf8(utf8File);

	char strTemp[10240] = "";
	wchar_t strLine[10240] = L"";
	wchar_t* t;
	int line = 0;
	wchar_t inside = INSIDE_NOTHING;
	CheatInfo* pCurrentCheat = NULL;

	while (1) {
		if (file->gets(strTemp, sizearray(strTemp))) {
			break;
		}
		wcscpy(strLine, utf8File ? U8toW(strTemp) : AtoW(strTemp));

		line++;

		size_t len = wcslen(strLine);
		// Get rid of the linefeed at the end
		while (len != 0 && (strLine[len - 1] == 0x0A || strLine[len - 1] == 0x0D)) {
			strLine[len - 1] = 0;
			len--;
		}

		wchar_t* s = strLine;										// Start parsing

		if (s[0] == '/' && s[1] == '/') {				// Comment
			continue;
		}

		if ((t = labelCheck(s, L"include")) != 0) {			// Include a file
			s = t;

			// Read name of the cheat file
			wchar_t* szQuote = NULL;
			quoteRead(&szQuote, NULL, s);

			Mem_File* mem_file = getIncludeFile(szQuote);
			if (!mem_file || parseMemCheatFile(mem_file)) {
				cheatError(szCheatFilename, line, NULL, L"included file doesn't exist", strLine);
			}

			continue;
		}

		if ((t = labelCheck(s, L"cheat")) != 0) {			// Add new cheat
			s = t;

			// Read cheat name
			wchar_t* szQuote = NULL;
			wchar_t* szEnd = NULL;

			quoteRead(&szQuote, &szEnd, s);

			s = szEnd;

			if ((t = labelCheck(s, L"advanced")) != 0) {		// Advanced cheat
				s = t;
			}

			SKIP_WS(s);

			if (inside == '{') {
				cheatError(szCheatFilename, line, pCurrentCheat, L"missing closing bracket", NULL);
				break;
			}

			inside = *s;

			// Link new node into the list
			CheatInfo* pPreviousCheat = pCurrentCheat;
			pCurrentCheat = new CheatInfo();
			if (pCheatInfo == NULL) {
				pCheatInfo = pCurrentCheat;
			}

			memset(pCurrentCheat, 0, sizeof(CheatInfo));
			pCurrentCheat->pPrevious = pPreviousCheat;
			if (pPreviousCheat) {
				pPreviousCheat->pNext = pCurrentCheat;
			}

			// Fill in defaults
			pCurrentCheat->nType = 0;							// Default to cheat type 0 (apply each frame)
			pCurrentCheat->nStatus = -1;						// Disable cheat

			memcpy(pCurrentCheat->szCheatName, szQuote, QUOTE_MAX);

			continue;
		}

		if ((t = labelCheck(s, L"type")) != 0) {				// Cheat type
			if (inside == INSIDE_NOTHING || pCurrentCheat == NULL) {
				cheatError(szCheatFilename, line, pCurrentCheat, L"rogue cheat type", strLine);
				break;
			}

			s = t;

			// Set type
			pCurrentCheat->nType = wcstol(s, NULL, 0);

			continue;
		}

		if ((t = labelCheck(s, L"default")) != 0) {			// Default option
			if (inside == INSIDE_NOTHING || pCurrentCheat == NULL) {
				cheatError(szCheatFilename, line, pCurrentCheat, L"rogue default", strLine);
				break;
			}

			s = t;

			// Set default option
			pCurrentCheat->nDefault = wcstol(s, NULL, 0);

			continue;
		}

		int n = wcstol(s, &t, 0);
		if (t != s) {				   							// New option
			if (inside == INSIDE_NOTHING || pCurrentCheat == NULL) {
				cheatError(szCheatFilename, line, pCurrentCheat, L"rogue option", strLine);
				break;
			}

			// Link a new Option structure to the cheat
			if (n < CHEAT_MAX_OPTIONS) {
				s = t;

				// Read option name
				wchar_t* szQuote = NULL;
				wchar_t* szEnd = NULL;
				if (quoteRead(&szQuote, &szEnd, s)) {
					cheatError(szCheatFilename, line, pCurrentCheat, L"option name omitted", strLine);
					break;
				}
				s = szEnd;

				if (pCurrentCheat->pOption[n] == NULL) {
					pCurrentCheat->pOption[n] = new CheatOption();
				}
				memset(pCurrentCheat->pOption[n], 0, sizeof(CheatOption));

				memcpy(pCurrentCheat->pOption[n]->szOptionName, szQuote, QUOTE_MAX * sizeof(wchar_t));

				int nCurrentAddress = 0;
				bool bOK = true;
				while (nCurrentAddress < CHEAT_MAX_ADDRESS) {
					int nCPU = 0, nAddress = 0, nValue = 0;

					if (skipComma(&s)) {
						nCPU = wcstol(s, &t, 0);		// CPU number
						if (t == s) {
							cheatError(szCheatFilename, line, pCurrentCheat, L"CPU number omitted", strLine);
							bOK = false;
							break;
						}
						s = t;

						skipComma(&s);
						nAddress = wcstol(s, &t, 0);	// Address
						if (t == s) {
							bOK = false;
							cheatError(szCheatFilename, line, pCurrentCheat, L"address omitted", strLine);
							break;
						}
						s = t;

						skipComma(&s);
						nValue = wcstol(s, &t, 0);		// Value
						if (t == s) {
							bOK = false;
							cheatError(szCheatFilename, line, pCurrentCheat, L"value omitted", strLine);
							break;
						}
					} else {
						if (nCurrentAddress) {			// Only the first option is allowed no address
							break;
						}
						if (n) {
							bOK = false;
							cheatError(szCheatFilename, line, pCurrentCheat, L"CPU / address / value omitted", strLine);
							break;
						}
					}

					pCurrentCheat->pOption[n]->AddressInfo[nCurrentAddress].nCPU = nCPU;
					pCurrentCheat->pOption[n]->AddressInfo[nCurrentAddress].nAddress = nAddress;
					pCurrentCheat->pOption[n]->AddressInfo[nCurrentAddress].nValue = nValue;
					nCurrentAddress++;
				}

				if (!bOK) {
					break;
				}
			}

			continue;
		}

		SKIP_WS(s);
		if (*s == '}') {
			if (inside != '{') {
				cheatError(szCheatFilename, line, pCurrentCheat, L"missing opening bracket", NULL);
				break;
			}

			inside = INSIDE_NOTHING;
		}
	}

	return 0;
}

static int getCheatArcName(char* name)
{
	if (!name) {
		return 1;
	}

	char arcName[256];
	sprintf(arcName, "%ls", getMiscArchiveName(PATH_CHEAT));
	sprintf(name, "%s%s", WtoA(getMiscPath(PATH_CHEAT)), arcName);

	int ret = archiveLoad.check(name);
	if (ret == ArchiveLoad::ARC_NONE) {
		return 1; // no archive
	}
	return 0;
}

static int cheatLoadArchive()
{
	char szCheatArcName[MAX_PATH] = "";
	getCheatArcName(szCheatArcName);

	swprintf(szCheatFilename, L"%hs.ini", BurnDrvGetTextA(DRV_NAME));
	void* buf = NULL;
	unsigned size = 0;
	if (archiveLoad.loadOneFile(szCheatArcName, WtoA(szCheatFilename), &buf, &size)) {
		return 1;
	}

	Mem_File* mem_file = new Mem_File((char*)buf, size);
	cheatTempFiles.push_back(mem_file);

	return parseMemCheatFile(mem_file);
}

static Mem_File* getIncludeFile(const wchar_t* name)
{
	swprintf(szCheatFilename, L"%s%s.ini", getMiscPath(PATH_CHEAT), name);

	void* buf = NULL;
	unsigned size = getFileBuffer(szCheatFilename, &buf);
	if (size) {
		Mem_File* mem_file = new Mem_File((char*)buf, size);
		cheatTempFiles.push_back(mem_file);
		return mem_file;
	}

	// load from archive
	char szCheatArcName[MAX_PATH] = "";
	getCheatArcName(szCheatArcName);

	swprintf(szCheatFilename, L"%s.ini", name);
	if (archiveLoad.loadOneFile(szCheatArcName, WtoA(szCheatFilename), &buf, &size)) {
		return NULL;
	}

	// write file content to buffer
	Mem_File* mem_file = new Mem_File((char*)buf, size);
	cheatTempFiles.push_back(mem_file);
	return mem_file;
}

int clearCheatTempFiles()
{
	for (size_t i = 0; i < cheatTempFiles.size(); i++) {
		delete cheatTempFiles[i];
	}
	cheatTempFiles.clear();
	return 0;
}
// <== load cheat from memory

int configCheatLoad(const wchar_t* filename)
{
	if (!filename) {
		swprintf(szCheatFilename, L"%s%hs.ini", getMiscPath(PATH_CHEAT), BurnDrvGetTextA(DRV_NAME));
	} else {
		wcsncpy(szCheatFilename, filename, sizearray(szCheatFilename));
	}

	Mem_File* mem_file = NULL;

	// write file content to buffer
	void* buf = NULL;
	int size = getFileBuffer(szCheatFilename, &buf);
	if (size) {
		mem_file = new Mem_File((char*)buf, size);
		cheatTempFiles.push_back(mem_file);
	}

	if (parseMemCheatFile(mem_file)) {
		// try to load cheat from archive files
		if (cheatLoadArchive()) {
			return 1;
		}
	}

	clearCheatTempFiles();

	if (pCheatInfo) {
		int nCurrentCheat = 0;
		while (cheatEnable(nCurrentCheat, -1) == 0) {
			nCurrentCheat++;
		}

		cheatUpdate();
	}

	return 0;
}

// cheat reload, added by regret
int configCheatReload(const wchar_t* filename)
{
	cheatExit(false);
	clearCheatTempFiles();

	cheatInit();
	configCheatLoad(filename);
	return 0;
}
